Skip to content

Add TeleChat - #50477

Closed
hmellor wants to merge 17 commits into
huggingface:mainfrom
hmellor:add-llama-aliases
Closed

hmellor wants to merge 17 commits into
huggingface:mainfrom
hmellor:add-llama-aliases

Conversation

@hmellor

@hmellor hmellor commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds native support for TeleChat (v1, TeleChat2 and TeleChat2.5, model_type: "telechat"). vLLM's native backend loads it into Llama's implementation, and this lets vLLM serve it through its Transformers backend without remote code. InternLM3 (#50578), IQuestCoder (#50579) and TeleChat3 (#50580) were split out of this PR.

One model covers every checkpoint with model_type: "telechat", except TeleChat-52B. That is a different architecture, and its config is rejected with a pointer to trust_remote_code. Differences from Llama:

  • Weights: only o_proj and down_proj have biases, so the attention and MLP __init__s differ from Llama's. Their forwards are Llama's.
  • Conversion on load: the original BLOOM-style names (transformer.h.N.self_attention.{query,dense}, word_embeddings, ln_f) are renamed. The fused self_attention.key_value is laid out per head ([k_0, v_0, k_1, v_1, …]). A TeleChat-only TeleChatInterleaveKeyValue op regroups it so that Chunk can split it into k_proj and v_proj. save_pretrained writes the original layout back, and base-model-only checkpoints (Tele-AI/telechat-7B) also load. vLLM's Transformers backend needs [Transformers backend] Apply Transformers' WeightConverters when loading weights vllm-project/vllm#66811 to apply the converter.
  • Config: the original keys (n_layer, n_head, ffn_hidden_size, layer_norm_epsilon, seq_length) are mapped explicitly in __post_init__. rope_scaling is ignored, because the original code never reads it.
  • RoPE: training_seqlen and base_seqlen move into rope_parameters:
-training_seqlen: 8192, base_seqlen: 8192, seq_length: 32768
+max_position_embeddings: 32768
+rope_parameters: {rope_type: "telechat_ntk", rope_theta, original_max_position_embeddings: 8192, base_seqlen: 8192}

Once the sequence outgrows original_max_position_embeddings, the original code applies a stepwise NTK scaling. TeleChatRotaryEmbedding reproduces it:

L = max(seq_len, original_max_position_embeddings)
ntk_alpha = 2 ** ceil(log2(L / base_seqlen) + 1) - 1     # base_seqlen defaults to 4096, as in TeleChat v1
base = rope_theta * ntk_alpha ** (dim / (dim - 2))
cos, sin *= 0.1 * ln(L / original_max_position_embeddings) + 1

The frequencies are cached in the inv_freq buffer, like dynamic_rope_update:

forward(x, position_ids)
  L = max(position_ids.max() + 1, original_max_position_embeddings)
  if L != cached L
    inv_freq, scaling = original (L at the training length) or recomputed for L
  # under torch.compile the cache check can't be traced, so inv_freq and scaling are recomputed instead

Unlike dynamic_rope_update, this rescales for the exact current length, as the original does, not the longest length seen so far.

Tokenizers

The checkpoints' tokenizer_config.json names Telechat2Tokenizer, which only exists in their remote code. telechat is added to MODELS_WITH_INCORRECT_HUB_TOKENIZER_CLASS, so the tokenizer loads as TokenizersBackend from the bare tokenizer.model without trust_remote_code. That conversion only matches the original tokenizer with #50555. Merge #50555 first.

Supersedes the stale v4-based #35694.

Evidence

Random tiny models built with each of the 5 versions of the original code. With training_seqlen=64, a 200-token input puts every position past it (NTK alpha 15, mscale 1.11), and the cached greedy decode crosses it:

Original code from Logits max rel. diff …with the original's fp16 cos/sin rounding mimicked Cached greedy decode
telechat-7B (v1) 6.6e-4 1.4e-6 identical*
TeleChat2-3B 5.9e-4 1.2e-6 identical
TeleChat2-115B (GQA) 5.5e-4 1.2e-6 identical
TeleChat2.5-35B 7.6e-4 1.3e-6 identical
TeleChat2.5-115B (GQA) 7.1e-4 1.3e-6 identical

All of the remaining difference is fp16 rounding. The original rounds cos/sin to fp16 before and after applying mscale, which the native implementation deliberately doesn't do. *The original v1 code crashes once cached decoding passes training_seqlen, because its rotary embedding uses the chunk length rather than the total length. So that comparison covers the 5 steps it completes.

Real weights:

Checkpoint dtype Argmax agreement (10 / ~300 / ~10K tokens) Cached greedy decode
Tele-AI/TeleChat2-3B (10K tokens > 8K training_seqlen) fp16* 100% / 99.7% / 100% identical
Tele-AI/telechat-7B (v1, NTK alpha 3 at any length) fp16* 100% / 100% / 100% identical
Tele-AI/TeleChat2-7B-32K bf16 100% / 99.3% / — identical

*The eager path of these versions of the original code only works in fp16.

  • The cached rotary embedding gives the same cos/sin as recomputing for every length, for sequences that grow, shrink and cross the training length.
  • TeleChat2-3B's Hub config loads as max_position_embeddings=32768 and rope_parameters={"rope_type": "telechat_ntk", "rope_theta": 10000.0, "original_max_position_embeddings": 8192, "base_seqlen": 8192}.
  • RUN_SLOW=1 integration tests (logits and generation for TeleChat2-3B, default dtype="auto", which loads the bf16 weights): 2 passed on B200, after the RoPE caching change.
  • Fast tests: the common suite, plus the NTK formula checked against the original at boundary lengths. TeleChatInterleaveKeyValue has a split-and-merge round-trip unit test.

Merge Danger

Door: two-way

A new model type, a new telechat entry in conversion_mapping and a new TeleChat-only conversion op. Depends on #50555 for tokenization. For vLLM, TeleChat also needs vllm-project/vllm#66811.

Blast Radius: new model

Loading these checkpoints without trust_remote_code now resolves to the native config, model and tokenizer, where it previously required remote code. With trust_remote_code=True, auto_map still takes precedence. For TeleChat-52B (same model_type), the config now fails with an explicit error instead of a remote-code prompt, unless trust_remote_code=True is passed.

🤖 Generated with Claude Code

hmellor and others added 2 commits November 8, 2026 12:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HuggingFaceDocBuilderDev

HuggingFaceDocBuilderDev commented Nov 8, 2026 •

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Pages changed in this PR:

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hmellor hmellor changed the title Add InternLM3, IQuestCoder and TeleChat3 Add InternLM3, IQuestCoder, TeleChat and TeleChat3 Nov 8, 2026
The checkpoints' tokenizer_config.json names tokenizer classes that only
exist in their remote code. They are thin SentencePiece wrappers, so route
these model types to TokenizersBackend and make the generic conversion from
a bare tokenizer.model follow the SentencePiece model's own settings:

- extra whitespace removal, dummy prefix and whitespace escaping from the
  normalizer spec
- BPE merges ranked by piece score
- user-defined pieces emitted whole and never merged
- added tokens matched on the raw text when there is a dummy prefix
- added_tokens_decoder may swap two pieces' ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hmellor added a commit to hmellor/vllm that referenced this pull request Nov 8, 2026
Their tokenizers now load natively too (huggingface/transformers#50477), so
the test entries no longer set trust_remote_code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
…inst the tokenized text

The dummy-prefix `Strip` decoder applies per token, so without byte fallback (and its `Fuse`) it stripped
the space from every word. The replacement offsets test decoded the input ids to recover the expanded text,
which only works for tokenizers that round-trip their input; SentencePiece-based ones don't around added tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hmellor added a commit to hmellor/vllm that referenced this pull request Nov 9, 2026
…ers modeling backend

These need huggingface/transformers#50477.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
@hmellor
hmellor requested a review from vasqu November 9, 2026 17:58

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not 100% sure here because a lot we probably could cover by using conversion mappings + updating the remote configs. The question is: Are they open to change their model type on the hub?

My gripe essentially is that we add 4 times llama but just slightly different configs to describe the same things. If we get other options to do it, I'd really love to ngl. Even frankenmerged configs (so the original entries + the entries we would use on transformers side living side by side)

This doesnt mean I'm opposed but we need to be careful to not add the same model again and again

hmellor and others added 6 commits November 10, 2026 11:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They only differ from Llama in their configs, so keep only the configs and point the auto mappings at the
Llama classes. InternLM3's `qkv_bias` and `bias` are loaded as `attention_bias` and `mlp_bias`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The original checkpoints fuse keys and values per head (`[k_0, v_0, k_1, v_1, ...]`). Split them into
`k_proj` and `v_proj` on load, with `Interleave` now taking a block size, so TeleChat uses Llama's
attention forward.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their configs are plain `PreTrainedConfig`s, so modular adds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They run the Llama implementation, which Llama's own tests cover. The config-specific behaviour is covered
by the fast tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The common tests re-ran Llama's suite with Llama-equivalent configs. TeleChat3's RoPE conversion is the only
config logic worth a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: auto, telechat, telechat3

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round 🫡

Comment thread docs/source/en/model_doc/iquestcoder.md Resolved
Comment thread docs/source/en/model_doc/iquestcoder.md Resolved
Comment thread docs/source/en/model_doc/telechat.md Resolved
Comment thread docs/source/en/_toctree.yml Resolved
hmellor and others added 2 commits November 10, 2026 14:58
…ched TeleChat RoPE

- Restore the modular files of InternLM3, IQuestCoder and TeleChat3, inheriting from `LlamaConfig`
- Revert the `Interleave` changes and add a TeleChat-only `TeleChatInterleaveKeyValue` op
- Map TeleChat's original config keys explicitly instead of via `attribute_map`
- Move `training_seqlen` and `base_seqlen` into `rope_parameters` (`"rope_type": "telechat_ntk"`)
- Cache the TeleChat RoPE frequencies, recomputing them only when the length beyond training changes
- Trim the TeleChat tests, use `Expectations` and `MemoryCleanupMixin`, drop the TeleChat3 config tests
- Use note/warning callouts in the docs and drop the conversion paragraph from the TeleChat docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR now only adds TeleChat. Also document TeleChat's RoPE parameters under `rope_parameters`, since the
docstring checker drops free text from config docstrings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 10, 2026
@hmellor hmellor changed the title Add InternLM3, IQuestCoder, TeleChat and TeleChat3 Add TeleChat Nov 10, 2026

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not so happy with the rope, maybe @zucchini-nlp has a better idea here

Also best to check whether TP really works as expected here (meaning along the new interleave op)

Comment on lines +86 to +97
if "n_layer" in kwargs:
self.num_hidden_layers = kwargs.pop("n_layer")
if "n_head" in kwargs:
self.num_attention_heads = kwargs.pop("n_head")
if "ffn_hidden_size" in kwargs:
self.intermediate_size = kwargs.pop("ffn_hidden_size")
if "layer_norm_epsilon" in kwargs:
self.rms_norm_eps = kwargs.pop("layer_norm_epsilon")
if "seq_length" in kwargs:
self.max_position_embeddings = kwargs.pop("seq_length")
if self.max_position_embeddings is None:
self.max_position_embeddings = kwargs.get("training_seqlen", 8192)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be simplified a bit but no biggie

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attribute map is better imo if we're just renaming stuff

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It used to be an attribute map but I was asked to remove it 😅

Comment on lines +128 to +131
# The original code supports these options, but no released checkpoint enables them
for option in ("embed_layernorm", "apply_residual_connection_post_layernorm"):
if getattr(self, option, False):
raise ValueError(f"TeleChat does not support `{option}`.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo let's just ignore?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done so that users (however rare they may be) coming from the custom code don't expect these custom code features to work

raise ValueError(f"TeleChat does not support `{option}`.")


class TeleChatRotaryEmbedding(nn.Module):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm still not a super fan of this, ig we need to always compute the freq? Is that really needed

cc @zucchini-nlp if you have good ideas on introducing the custom rope here

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can do as in Hunyuan's ntk scaled rope

# Diff from Llama - DynamicNTKAlphaRotary. The checkpoints label it "dynamic", but a fixed `alpha` makes the
# frequencies static, so we give it its own type to keep `dynamic_rope_update` from recomputing them.
if self.rope_type == "dynamic" and self.config.rope_parameters.get("alpha"):
self.rope_type = "ntk_alpha"
rope_init_fn: Callable = self.compute_default_rope_parameters
if self.rope_type == "ntk_alpha":
rope_init_fn = self.compute_ntk_alpha_rope_parameters
elif self.rope_type != "default":
rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
inv_freq, self.attention_scaling = rope_init_fn(self.config)

Comment thread src/transformers/core_model_loading.py Resolved

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we had a custom ntk rope with hunyuan so can accomodate imo, just needs a little tweaking to make rope module support all other types as well

raise ValueError(f"TeleChat does not support `{option}`.")


class TeleChatRotaryEmbedding(nn.Module):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can do as in Hunyuan's ntk scaled rope

# Diff from Llama - DynamicNTKAlphaRotary. The checkpoints label it "dynamic", but a fixed `alpha` makes the
# frequencies static, so we give it its own type to keep `dynamic_rope_update` from recomputing them.
if self.rope_type == "dynamic" and self.config.rope_parameters.get("alpha"):
self.rope_type = "ntk_alpha"
rope_init_fn: Callable = self.compute_default_rope_parameters
if self.rope_type == "ntk_alpha":
rope_init_fn = self.compute_ntk_alpha_rope_parameters
elif self.rope_type != "default":
rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
inv_freq, self.attention_scaling = rope_init_fn(self.config)

Comment on lines +143 to +145
# `_init_weights` recomputes the buffers with `compute_default_rope_parameters`
self.rope_type = "default"
inv_freq, _ = self.compute_default_rope_parameters(config)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah we dont want to silently force default when it is not default under the hood

Comment on lines +191 to +198
if is_torchdynamo_compiling():
# The cache lookup depends on the data, which can't be traced, so recompute the parameters instead
inv_freq, attention_scaling = self.compute_default_rope_parameters(
self.config, x.device, position_ids.max() + 1
)
else:
self.ntk_frequency_update(position_ids, x.device)
inv_freq, attention_scaling = self.inv_freq, self.attention_scaling

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here we could do the same

if not is_torchdynamo_compiling() and "ntk_custom_name" in rope_type:
    self.ntk_frequency_update(position_ids, x.device)
# keep the rest same as Llama and use `self.inv_freq` as it was updated by above call

Comment on lines +86 to +97
if "n_layer" in kwargs:
self.num_hidden_layers = kwargs.pop("n_layer")
if "n_head" in kwargs:
self.num_attention_heads = kwargs.pop("n_head")
if "ffn_hidden_size" in kwargs:
self.intermediate_size = kwargs.pop("ffn_hidden_size")
if "layer_norm_epsilon" in kwargs:
self.rms_norm_eps = kwargs.pop("layer_norm_epsilon")
if "seq_length" in kwargs:
self.max_position_embeddings = kwargs.pop("seq_length")
if self.max_position_embeddings is None:
self.max_position_embeddings = kwargs.get("training_seqlen", 8192)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attribute map is better imo if we're just renaming stuff

Comment on lines +116 to +118
def _validate_telechat_ntk_rope_parameters(self, rope_parameters: dict, ignore_keys: set[str] | None = None):
required_keys = {"rope_type", "rope_theta", "original_max_position_embeddings", "base_seqlen"}
self._check_received_keys("telechat_ntk", set(rope_parameters), required_keys, ignore_keys=ignore_keys)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_rope from here no? IF type==custom_nkt, do custom validation; else super().validate_rope

def validate_rope(self: "PreTrainedConfig") -> None:
"""
Validate the RoPE config arguments, given a `"PreTrainedConfig"` object
"""
# Don't validate if no rope_parameters found (`None`) or if it's an empty dict
# Note that validation runs every time a new config is created, even if config is non-RoPE
rope_parameters_dict = getattr(self, "rope_parameters", None)

Its fused key/value conversion runs on each rank's shard, which holds whole per-head key/value pairs, so the TP tests
check that it shards correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hmellor and others added 2 commits November 11, 2026 12:53
Its config has no dtype, but its weights are stored in bf16, so `dtype="auto"` loads them in bf16. The expected logits
are re-recorded on B200; the generated text is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python evaluates `dict.get`'s default before the lookup, so the pops nested in the defaults already ran and the trailing
pops were dead. Pop the original keys once as defaults and build `rope_parameters` from them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hmellor

hmellor commented Nov 11, 2026

Copy link
Copy Markdown
Member Author

Closing: vLLM is removing TeleChat2 and TeleChat3 (vllm-project/vllm#67035) because usage is very low, so native support here is no longer needed.

@hmellor hmellor closed this Nov 11, 2026
@hmellor
hmellor deleted the add-llama-aliases branch November 11, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants